iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Rust

大家一起跟Rust當好朋友吧!系列 第 6

Day 6: 結構 (Structs) 與列舉 (Enums):打造自己的資料型別

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第六天!

經過前五天的學習,我們已經掌握了 Rust 的基本語法、所有權系統和參考借用。今天我們要來學習如何建立自己的資料型別—這就是結構 (Structs) 和列舉 (Enums) 的魅力所在!

如果說前面學的是 Rust 提供的「積木」,那麼今天我們就要學會如何組合這些積木,打造出符合我們需求的「城堡」。對於習慣了 C# 的 class 的我們來說,Struct 可能會讓你覺得既熟悉又陌生—熟悉的是它同樣可以組織相關的資料,陌生的是它的設計概念更偏向於「資料」而並非「行為」。

而 Enum 更是 Rust 的一大亮點!它不僅僅是一組常數的集合,而是一個功能強大的型別系統工具,能讓我們優雅地處理各種可能的情況。

結構 (Structs):組織相關資料

基本結構語法

讓我們從一個簡單的例子開始:

struct User {
    username: String,
    email: String,
    age: u32,
    active: bool,
}

fn main() {
    let user1 = User {
        username: String::from("alice123"),
        email: String::from("alice@example.com"),
        age: 25,
        active: true,
    };
    
    println!("使用者:{}", user1.username);
    println!("信箱:{}", user1.email);
    println!("年齡:{}", user1.age);
    println!("狀態:{}", if user1.active { "活躍" } else { "非活躍" });
}

可變結構

就像變數一樣,如果要修改結構的欄位,整個實例都必須是可變的:

fn main() {
    let mut user1 = User {
        username: String::from("alice123"),
        email: String::from("alice@example.com"),
        age: 25,
        active: true,
    };
    
    user1.age = 26;  // 修改年齡
    user1.email = String::from("alice_new@example.com");  // 修改信箱
    
    println!("更新後的年齡:{}", user1.age);
}

注意:Rust 不允許只將結構的某些欄位標記為可變,要嘛整個實例都可變,要嘛都不可變。

結構與函式

我們可以建立函式來方便地建立結構實例:

fn build_user(email: String, username: String) -> User {
    User {
        username: username,
        email: email,
        age: 18,
        active: true,
    }
}

// 使用欄位簡寫語法
fn build_user_short(email: String, username: String) -> User {
    User {
        username,  // 當參數名稱與欄位名稱相同時,可以簡寫
        email,
        age: 18,
        active: true,
    }
}

fn main() {
    let user2 = build_user(
        String::from("bob@example.com"),
        String::from("bob456")
    );
    
    println!("新用戶:{}", user2.username);
}

結構更新語法

有時候我們想要基於既有的結構建立新的實例,只修改部分欄位:

fn main() {
    let user1 = User {
        username: String::from("alice123"),
        email: String::from("alice@example.com"),
        age: 25,
        active: true,
    };
    
    // 基於 user1 建立 user2,只修改 username 和 email
    let user2 = User {
        username: String::from("charlie789"),
        email: String::from("charlie@example.com"),
        ..user1  // 其餘欄位使用 user1 的值
    };
    
    println!("用戶2的年齡:{}", user2.age);  // 25(從 user1 複製)
    
    // 注意:user1 中包含 String 的欄位已經被移動,但 age 和 active 被複製
    // println!("{}", user1.username);  // 錯誤!username 已被移動
    println!("{}", user1.age);        // 沒問題,age 實現了 Copy
}

元組結構

有時候我們只需要組合一些型別,但不需要為每個欄位命名:

struct Color(i32, i32, i32);      // RGB 顏色
struct Point(f64, f64, f64);      // 3D 座標

fn main() {
    let red = Color(255, 0, 0);
    let origin = Point(0.0, 0.0, 0.0);
    
    println!("紅色的 R 值:{}", red.0);
    println!("原點的 X 座標:{}", origin.0);
}

單元結構

有時候我們需要一個沒有任何欄位的結構,通常用於實現 trait:

struct UnitStruct;

fn main() {
    let unit = UnitStruct;
}

為結構新增功能

使用 impl 區塊定義方法

我們可以為結構定義方法和關聯函式:

#[derive(Debug)]  // 讓結構可以用 {:?} 印出
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // 關聯函式(類似靜態方法)
    fn new(width: f64, height: f64) -> Rectangle {
        Rectangle { width, height }
    }
    
    fn square(size: f64) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
    
    // 方法(第一個參數是 &self)
    fn area(&self) -> f64 {
        self.width * self.height
    }
    
    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }
    
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width >= other.width && self.height >= other.height
    }
    
    // 可變方法
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    // 使用關聯函式建立實例
    let rect1 = Rectangle::new(30.0, 50.0);
    let square = Rectangle::square(25.0);
    
    println!("長方形:{:?}", rect1);
    println!("正方形:{:?}", square);
    
    // 使用方法
    println!("長方形面積:{:.2}", rect1.area());
    println!("長方形周長:{:.2}", rect1.perimeter());
    println!("長方形能包含正方形嗎?{}", rect1.can_hold(&square));
    
    // 可變方法
    let mut rect2 = Rectangle::new(10.0, 20.0);
    println!("縮放前:{:?}", rect2);
    rect2.scale(2.0);
    println!("縮放後:{:?}", rect2);
}

列舉 (Enums):表達多種可能性

Rust 的列舉比許多語言的列舉都要強大得多。它不僅可以列出可能的值,每個變體還可以攜帶資料!

基本列舉

#[derive(Debug)]
enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;
    
    println!("IPv4:{:?}", four);
    println!("IPv6:{:?}", six);
}

攜帶資料的列舉

這是 Rust 列舉的強大之處—每個變體都可以攜帶不同型別和數量的資料:

#[derive(Debug)]
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

#[derive(Debug)]
enum Message {
    Quit,                        // 沒有資料
    Move { x: i32, y: i32 },     // 命名欄位
    Write(String),               // 單一字串
    ChangeColor(i32, i32, i32),  // 三個整數
}

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
    
    println!("本機 IPv4:{:?}", home);
    println!("環路 IPv6:{:?}", loopback);
    
    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::Write(String::from("Hello"));
    let msg4 = Message::ChangeColor(255, 0, 0);
    
    println!("訊息:{:?}, {:?}, {:?}, {:?}", msg1, msg2, msg3, msg4);
}

為列舉實現方法

列舉也可以有方法:

impl Message {
    fn process(&self) {
        match self {
            Message::Quit => println!("收到退出訊息"),
            Message::Move { x, y } => println!("移動到座標 ({}, {})", x, y),
            Message::Write(text) => println!("寫入文字:{}", text),
            Message::ChangeColor(r, g, b) => println!("改變顏色為 RGB({}, {}, {})", r, g, b),
        }
    }
}

fn main() {
    let messages = vec![
        Message::Quit,
        Message::Move { x: 10, y: 20 },
        Message::Write(String::from("Hello, Rust!")),
        Message::ChangeColor(255, 128, 0),
    ];
    
    for msg in messages {
        msg.process();
    }
}

Option<T> 列舉:優雅地處理空值

Rust 沒有 null 值!相反,它使用 Option<T> 列舉來表示可能存在或不存在的值:

fn main() {
    let some_number = Some(5);
    let some_string = Some("hello");
    let absent_number: Option<i32> = None;
    
    println!("數字:{:?}", some_number);
    println!("字串:{:?}", some_string);
    println!("空值:{:?}", absent_number);
    
    // 安全地處理可能為空的值
    let x = Some(5);
    let y = None;
    
    println!("x + 1 = {:?}", add_one(x));
    println!("y + 1 = {:?}", add_one(y));
}

fn add_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
        None => None,
    }
}

更方便的 Option 方法

Option<T> 提供了許多便利方法:

fn main() {
    let name = Some("Alice");
    let age: Option<u32> = None;
    
    // unwrap_or:提供預設值
    println!("年齡:{}", age.unwrap_or(0));
    
    // map:轉換 Some 中的值
    let name_length = name.map(|n| n.len());
    println!("名字長度:{:?}", name_length);
    
    // is_some / is_none:檢查是否有值
    if name.is_some() {
        println!("有名字!");
    }
    
    if age.is_none() {
        println!("沒有年齡資訊");
    }
    
    // and_then:鏈式操作
    let result = name
        .and_then(|n| if n.len() > 3 { Some(n.to_uppercase()) } else { None });
    println!("處理結果:{:?}", result);
}

match 表達式:模式匹配的威力

match 是 Rust 中處理列舉最強大的工具:

#[derive(Debug)]
enum Coin {
    OneDollar,     // 1元硬幣
    FiveDollar,    // 5元硬幣
    TenDollar,     // 10元硬幣
    FiftyDollar(String),  // 50元硬幣,攜帶發行年份或特殊版本
}

fn value_in_dollars(coin: Coin) -> u32 {
    match coin {
        Coin::OneDollar => {
            println!("幸運硬幣!");
            1
        },
        Coin::FiveDollar => 5,
        Coin::TenDollar => 10,
        Coin::FiftyDollar(version) => {
            println!("來自 {} 的 50 元硬幣!", version);
            50
        },
    }
}

fn main() {
    let coins = vec![
        Coin::OneDollar,
        Coin::FiveDollar,
        Coin::FiftyDollar(String::from("民國111年")),
        Coin::TenDollar,
    ];
    
    let total: u32 = coins.into_iter()
        .map(|coin| value_in_dollars(coin))
        .sum();
    
    println!("總價值:{} 元", total);
}

if let 語法糖

當我們只關心一個特定的模式時,可以使用 if let

fn main() {
    let some_value = Some(3);
    
    // 使用 match
    match some_value {
        Some(3) => println!("三!"),
        _ => (),
    }
    
    // 使用 if let(更簡潔)
    if let Some(3) = some_value {
        println!("又是三!");
    }
    
    // 處理 Option
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("最大值設定為:{}", max);
    }
}

實戰練習:建立部落格文章模型

讓我們用今天學到的知識建立一個簡單的部落格文章模型:這個程式展示了今天學到的所有重要概念:

1. 結構的設計

  • Author 結構包含作者的基本資訊
  • BlogPost 結構組織了文章的所有相關資料
  • Blog 結構管理多篇文章,使用 HashMap 作為儲存

2. 列舉的應用

  • PostStatus 列舉表達文章的三種狀態
  • 使用 matchmatches! 宏來處理不同狀態

3. 方法與關聯函式

  • new 關聯函式作為建構函式
  • 各種方法提供物件的行為(發布、封存、增加標籤等)

4. Option<T> 的實際應用

  • 搜尋文章時可能找不到,回傳 Option<&BlogPost>
  • 使用 if let 優雅地處理可能的空值

5. 所有權與借用的實踐

  • 使用參考來避免不必要的所有權轉移
  • 適當使用可變借用來修改資料

進階技巧:derive 屬性

我們在例子中使用了 #[derive(Debug)],這是 Rust 的一個強大功能,可以自動生成常見的 trait 實現:

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = p1.clone();  // Clone trait
    
    println!("{:?}", p1);  // Debug trait
    println!("p1 == p2: {}", p1 == p2);  // PartialEq trait
}

常用的可 derive 的 trait:

  • Debug:讓型別可以用 {:?} 印出
  • Clone:提供 clone() 方法來複製值
  • Copy:讓型別可以被隱式複製(只適用於簡單型別)
  • PartialEq:提供 ==!= 運算子
  • Eq:表示完全相等性
  • PartialOrdOrd:提供比較運算子

今天的收穫

今天我們學會了 Rust 中最重要的自訂型別:

結構 (Structs)

  • 使用 struct 組織相關資料
  • 三種結構:命名欄位、元組結構、單元結構
  • 使用 impl 區塊為結構添加方法和關聯函式
  • 結構更新語法 .. 來基於既有實例建立新實例

列舉 (Enums)

  • 表達多種可能的狀態或變體
  • 每個變體可以攜帶不同型別的資料
  • 使用 match 進行模式匹配
  • if let 語法糖處理簡單的模式

Option 型別

  • Rust 處理空值的安全方式
  • 強制我們明確處理可能不存在的值
  • 提供豐富的方法來操作可選值

實用技巧

  • #[derive(...)] 自動生成常見 trait 實現
  • 模式匹配的威力與靈活性
  • 如何設計清晰的資料模型

明天我們將進入第一週的總結,並深入學習 Cargo 這個強大的建置工具與套件管理器。我們會學習如何管理相依性、執行測試、並為我們的專案建立良好的結構。

今天的小挑戰

嘗試擴展我們的部落格模型,增加以下功能:

  1. 評論系統:建立 Comment 結構,包含作者、內容、時間戳記
  2. 文章分類:建立 Category 列舉,並為文章添加分類功能
  3. 使用者角色:建立 Role 列舉(Admin, Editor, Writer, Reader),並為 Author 添加角色欄位
  4. 互動功能:為文章添加點讚/踩功能

提示:

  • 思考哪些資料應該組織成結構,哪些應該用列舉
  • 考慮如何使用 Option<T> 來處理可選資料
  • 嘗試為新的型別實現有用的方法
// 範例結構
struct Comment {
    id: u32,
    author: Author,
    content: String,
    // 你來完成剩下的欄位和功能!
}

enum Category {
    Technology,
    Lifestyle,
    // 添加更多分類!
}

enum Role {
    Admin,
    Editor,
    // 添加更多角色!
}

如果在實作過程中遇到任何問題,歡迎在留言區討論。結構和列舉是 Rust 程式設計的基礎,熟練掌握它們會讓你在後續的學習中如魚得水!

記住,好的型別設計能讓程式碼更易讀、更安全、更易維護。花時間思考如何合理地組織你的資料結構,絕對是值得的投資。

我們明天見!


上一篇
Day 5: 參考 (References) 與借用 (Borrowing):不轉移所有權的資料傳遞
下一篇
Day 7: 第一週回顧與 Cargo 工具鏈:掌握 Rust 開發的瑞士軍刀
系列文
大家一起跟Rust當好朋友吧!20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言